﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Collections;

namespace Kokomo.Mirror
{
	/// <summary>
	/// Serializes and deserializes binary structures.
	/// </summary>
	/// <remarks>
	/// The serialization is controlled by marking the properties of the structure with special attributes.
	/// </remarks>
	/// <seealso cref="SerializedArrayAttribute"/>
	/// <seealso cref="SerializedConditionalAttribute"/>
	/// <seealso cref="SerializedConditionAttribute"/>
	/// <seealso cref="SerializedOffsetAttribute"/>
	public class StructSerializer
	{
		/// <summary>
		/// Initializes a new <see cref="StructSerializer"/>.
		/// </summary>
		/// <param name="encoding">The encoding of strings, or <c>null</c>.</param>
		public StructSerializer(
			Encoding encoding = null
			)
		{
			if (encoding == null) encoding = Encoding.Default;
			this.Encoding = encoding;
		}

		/// <summary>
		/// The encoding of strings within the structure.
		/// </summary>
		public Encoding Encoding { get; private set; }

		private StructSerializerContext CreateReadContext(Stream stream)
		{
			StructSerializerContext context = new StructSerializerContext()
			{
				serializer = this,
				stream = stream,
				reader = new BinaryReader(stream, this.Encoding)
			};
			return context;
		}

		/// <summary>
		/// Reads a structure from a stream.
		/// </summary>
		/// <typeparam name="TStruct">The type of structure to read.</typeparam>
		/// <param name="stream">The stream from which to read the structure.</param>
		/// <returns>The structure read from the stream.</returns>
		public TStruct ReadStruct<TStruct>(Stream stream)
			where TStruct : new()
		{
			if (stream == null) throw new ArgumentNullException("stream");

			TStruct struc = new TStruct();
			this.ReadStruct(struc, stream);
			return struc;
		}

		/// <summary>
		/// Reads a structure from a stream.
		/// </summary>
		/// <param name="structType">The type of structure to read.</param>
		/// <param name="stream">The stream from which to read the structure.</param>
		/// <returns></returns>
		public object ReadStruct(Type structType, Stream stream)
		{
			if ((object)structType == null) throw new ArgumentNullException("structType");
			if (stream == null) throw new ArgumentNullException("stream");

			object instance = Activator.CreateInstance(structType);
			this.ReadStruct(instance, stream);
			return instance;
		}

		/// <summary>
		/// Reads a structure from a stream.
		/// </summary>
		/// <param name="instance">The structure to populate with data from the stream.</param>
		/// <param name="stream">The stream from which to read the structure.</param>
		public void ReadStruct(object instance, Stream stream)
		{
			if (instance == null) throw new ArgumentNullException("struc");
			if (stream == null) throw new ArgumentNullException("stream");

			StructSerializerContext context = this.CreateReadContext(stream);
			this.ReadStruct(instance, context);
		}

		/// <summary>
		/// Reads a structure.
		/// </summary>
		/// <param name="instance">The structure instance to populate.</param>
		/// <param name="context">The context.</param>
		private void ReadStruct(object instance, StructSerializerContext context)
		{
			BinaryReader reader = context.reader;

			Type structType = instance.GetType();
			MemberInfo[] members = structType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			Dictionary<string, bool> booleanFieldValues = new Dictionary<string, bool>();
			Dictionary<string, int> numericFieldValues = new Dictionary<string, int>();
			foreach (var member in members)
			{
				SerializedConditionAttribute conditionAttribute = (SerializedConditionAttribute)Attribute.GetCustomAttribute(member, typeof(SerializedConditionAttribute));
				if (conditionAttribute != null)
				{
					bool conditionValue = (bool)this.GetPropertyPathValue(instance, conditionAttribute.ConditionPropertyPath);
					if (!conditionValue) continue;
				}
				SerializedOffsetAttribute offsetAttribute = (SerializedOffsetAttribute)Attribute.GetCustomAttribute(member, typeof(SerializedOffsetAttribute));
				if (offsetAttribute != null)
				{
					int offset = Convert.ToInt32(this.GetPropertyPathValue(instance, offsetAttribute.OffsetPropertyName));
					context.stream.Position = offset;
				}

				PropertyInfo property = member as PropertyInfo;
				if (property != null)
				{
					MethodInfo setter = property.GetSetMethod(true);
					if (setter == null) continue;

					NonSerializedAttribute nonSerializable = property.GetCustomAttribute<NonSerializedAttribute>();
					if (nonSerializable != null) continue;

					object propertyValue = null;

					SerializerContextFieldAttribute contextFieldAttribute = (SerializerContextFieldAttribute)Attribute.GetCustomAttribute(property, typeof(SerializerContextFieldAttribute));
					if (contextFieldAttribute != null)
					{
						switch (contextFieldAttribute.Field)
						{
							case SerializerContextField.Position32:
								propertyValue = (int)context.stream.Position;
								break;
							case SerializerContextField.Position64:
								propertyValue = context.stream.Position;
								break;
						}
					}
					else
					{
						propertyValue = this.ReadPropertyValue(property, property.PropertyType, instance, context);
					}
					if (propertyValue != null)
					{
						setter.Invoke(instance, new object[] { propertyValue });
					}
				}
			}
		}

		/// <summary>
		/// Reads a property value from a stream.
		/// </summary>
		/// <param name="property">The property to read.</param>
		/// <param name="propertyType">The type of value to read.</param>
		/// <param name="instance">The structure instance.</param>
		/// <returns>The property value.</returns>
		private object ReadPropertyValue(
			PropertyInfo property,
			Type propertyType,
			object instance,
			StructSerializerContext context
			)
		{
			object propertyValue = null;

			if (propertyType.IsArray)
			{
				int? length = this.DetermineLength(property, instance, context);
				propertyValue = this.ReadArray(property, instance, propertyType.GetElementType(), length, context);
			}
			else if (propertyType == typeof(string))
			{
				int? length = this.DetermineLength(property, instance, context);
				propertyValue = this.ReadString(length, context);
			}
			else if (propertyType.IsEnum || propertyType.IsPrimitive)
			{
				propertyValue = this.ReadScalarValue(propertyType, context);
			}
			else
			{
				SerializeSwitchAttribute[] switchAttributes = (SerializeSwitchAttribute[])Attribute.GetCustomAttributes(property, typeof(SerializeSwitchAttribute));
				if (switchAttributes.Length > 0)
				{
					foreach (var switchAttribute in switchAttributes)
					{
						bool switchValue = (bool)GetPropertyPathValue(instance, switchAttribute.ConditionPropertyPath);
						if (switchValue)
						{
							propertyType = switchAttribute.StructType;
							break;
						}
					}
				}

				object propertyStruc = Activator.CreateInstance(propertyType);
				this.ReadStruct(propertyStruc, context);
				propertyValue = propertyStruc;
			}

			return propertyValue;
		}

		/// <summary>
		/// Reads a scalar value.
		/// </summary>
		/// <param name="propertyType">The type of value to read.</param>
		/// <param name="context">The serializer context.</param>
		/// <returns>The value of the property.</returns>
		private object ReadScalarValue(
			Type propertyType,
			StructSerializerContext context
			)
		{
			object propertyValue = null;
			if (propertyType.IsPrimitive)
			{
				propertyValue = ReadPrimitive(propertyType, context);
			}
			else if (propertyType.IsEnum)
			{
				object numericValue = ReadPrimitive(propertyType.GetEnumUnderlyingType(), context);
				propertyValue = Enum.ToObject(propertyType, numericValue);
			}
			else
			{
				propertyValue = Activator.CreateInstance(propertyType);
				ReadStruct(propertyValue, context);
			}

			return propertyValue;
		}

		private object GetPropertyPathValue(object instance, string propertyPath)
		{
			if (instance == null) throw new ArgumentNullException("instance");
			if (propertyPath == null) throw new ArgumentNullException("propertyName");

			string[] propertyNames = propertyPath.Split('.');
			foreach (var propertyName in propertyNames)
			{
				PropertyInfo property = instance.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
				object value = property.GetValue(instance, null);
				if (value == null) break;
				instance = value;
			}

			return instance;
		}

		private int? DetermineLength(
			PropertyInfo property,
			object instance,
			StructSerializerContext context
			)
		{
			if (property == null) throw new ArgumentNullException("property");
			if (instance == null) throw new ArgumentNullException("instance");

			SerializedArrayAttribute serializedStringAttribute = (SerializedArrayAttribute)Attribute.GetCustomAttribute(property, typeof(SerializedArrayAttribute));
			if (serializedStringAttribute == null) throw new InvalidOperationException("String property missing SerializedStringAttribute");

			BinaryReader reader = context.reader;
			int? length = null;
			switch (serializedStringAttribute.LengthEncoding)
			{
				case ArrayLengthEncoding.Fixed:
					length = serializedStringAttribute.FixedLength;
					break;
				case ArrayLengthEncoding.ByteCountPrefix16:
				case ArrayLengthEncoding.ElementCountPrefix16:
					length = reader.ReadInt16();
					break;
				case ArrayLengthEncoding.ByteCountPrefix32:
					length = reader.ReadInt32();
					break;
				case ArrayLengthEncoding.ElementCountPrefix32:
					break;
				case ArrayLengthEncoding.BytePrefixField:
				case ArrayLengthEncoding.ElementPrefixField:
					object prefixFieldValue = this.GetPropertyPathValue(instance, serializedStringAttribute.LengthFieldName);
					length = Convert.ToInt32(prefixFieldValue);
					break;
				case ArrayLengthEncoding.ZeroTerminated:
					break;
			}

			return length;
		}

		private object ReadPrimitive(
			Type type,
			StructSerializerContext context
			)
		{
			if ((object)type == null) throw new ArgumentNullException("type");

			object value = null;
			BinaryReader reader = context.reader;
			if (type.IsPrimitive)
			{
				if (type == typeof(int))
				{
					value = reader.ReadInt32();
				}
				else if (type == typeof(IntPtr))
				{
					value = new IntPtr(reader.ReadInt32());
				}
				else if (type == typeof(uint))
				{
					value = reader.ReadUInt32();
				}
				else if (type == typeof(short))
				{
					value = reader.ReadInt16();
				}
				else if (type == typeof(ushort))
				{
					value = reader.ReadUInt16();
				}
				else if (type == typeof(byte))
				{
					value = reader.ReadByte();
				}
				else if (type == typeof(bool))
				{
					value = reader.ReadBoolean();
				}
				else if (type == typeof(long))
				{
					value = reader.ReadInt64();
				}
				else if (type == typeof(ulong))
				{
					value = reader.ReadUInt64();
				}
				else
				{
					throw new ArgumentException("Unknown type");
				}
			}

			return value;
		}

		private string ReadString(
			int? length,
			StructSerializerContext context
			)
		{
			string str;
			if (length.HasValue)
			{
				str = context.reader.ReadFixedLengthString(length.Value);
			}
			else
			{
				str = context.reader.ReadNullTerminatedString();
			}

			return str;
		}

		public TElement[] ReadArray<TElement>(
			int? length,
			Stream stream
			)
		{
			if (stream == null) throw new ArgumentNullException("stream");

			StructSerializerContext context = this.CreateReadContext(stream);
			return this.ReadArray<TElement>(length, context);
		}
		private TElement[] ReadArray<TElement>(
			int? length,
			StructSerializerContext context
			)
		{
			return (TElement[])this.ReadArray(typeof(TElement), length, ReadScalarValue, context);
		}
		private Array ReadArray(
			PropertyInfo property,
			object instance,
			Type elementType,
			int? length,
			StructSerializerContext context
			)
		{
			return this.ReadArray(elementType, length, (t, ctx) => ReadPropertyValue(property, t, instance, ctx), context);
		}
		private Array ReadArray(
			Type elementType,
			int? length,
			Func<Type, StructSerializerContext, object> readerDelegate,
			StructSerializerContext context
			)
		{
			if ((object)elementType == null) throw new ArgumentNullException("elementType");
			if (readerDelegate == null) throw new ArgumentNullException("readerDelegate");

			Array array;

			if (length.HasValue)
			{
				array = Array.CreateInstance(elementType, length.Value);

				for (int i = 0; i < array.Length; i++)
				{
					object element = readerDelegate(elementType, context);
					array.SetValue(element, i);
				}
			}
			else
			{
				List<object> list = new List<object>();
				object defaultInstance = Activator.CreateInstance(elementType);

				object element = null;
				while (true)
				{
					element = readerDelegate(elementType, context);
					if (object.Equals(element, defaultInstance)) break;
					list.Add(element);
				}

				array = Array.CreateInstance(elementType, list.Count);
				Array.Copy(list.ToArray(), array, array.Length);
			}

			return array;
		}

		#region Writing
		private StructSerializerContext CreateWriteContext(Stream stream)
		{
			StructSerializerContext context = new StructSerializerContext()
			{
				serializer = this,
				stream = stream,
				writer = new BinaryWriter(stream, this.Encoding)
			};
			return context;
		}

		/// <summary>
		/// Reads a structure.
		/// </summary>
		/// <param name="instance">The structure instance to populate.</param>
		/// <param name="context">The context.</param>
		public void WriteStruct(object instance, Stream stream)
		{
			if (instance == null) throw new ArgumentNullException("instance");
			if (stream == null) throw new ArgumentNullException("stream");

			StructSerializerContext context = this.CreateWriteContext(stream);
			this.WriteStruct(instance, context);
		}
		/// <summary>
		/// Reads a structure.
		/// </summary>
		/// <param name="instance">The structure instance to populate.</param>
		/// <param name="context">The context.</param>
		private void WriteStruct(object instance, StructSerializerContext context)
		{
			BinaryWriter writer = context.writer;

			Type structType = instance.GetType();
			MemberInfo[] members = structType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			Dictionary<string, bool> booleanFieldValues = new Dictionary<string, bool>();
			Dictionary<string, int> numericFieldValues = new Dictionary<string, int>();
			foreach (var member in members)
			{
				SerializedConditionAttribute conditionAttribute = (SerializedConditionAttribute)Attribute.GetCustomAttribute(member, typeof(SerializedConditionAttribute));
				if (conditionAttribute != null)
				{
					bool conditionValue = (bool)this.GetPropertyPathValue(instance, conditionAttribute.ConditionPropertyPath);
					if (!conditionValue) continue;
				}
				//SerializedOffsetAttribute offsetAttribute = (SerializedOffsetAttribute)Attribute.GetCustomAttribute(member, typeof(SerializedOffsetAttribute));
				//if (offsetAttribute != null)
				//{
				//    int offset = Convert.ToInt32(this.GetPropertyPathValue(instance, offsetAttribute.OffsetPropertyName));
				//    context.stream.Position = offset;
				//}

				PropertyInfo property = member as PropertyInfo;
				if (property != null)
				{
					MethodInfo getter = property.GetGetMethod(true);
					if (getter == null) continue;

					NonSerializedPropertyAttribute nonSerializable = property.GetCustomAttribute<NonSerializedPropertyAttribute>();
					if (nonSerializable != null) continue;

					object propertyValue = null;

					//SerializerContextFieldAttribute contextFieldAttribute = (SerializerContextFieldAttribute)Attribute.GetCustomAttribute(property, typeof(SerializerContextFieldAttribute));
					//if (contextFieldAttribute != null)
					//{
					//    switch (contextFieldAttribute.Field)
					//    {
					//        case SerializerContextField.Position32:
					//            propertyValue = (int)context.stream.Position;
					//            break;
					//        case SerializerContextField.Position64:
					//            propertyValue = context.stream.Position;
					//            break;
					//    }
					//}
					//else

					this.WritePropertyValue(
						property,
						instance,
						context
						);
				}
			}
		}

		private void WritePropertyValue(
			PropertyInfo property,
			object instance,
			StructSerializerContext context
			)
		{
			Type propertyType = property.PropertyType;
			object propertyValue = property.GetValue(instance, null);

			if (propertyType.IsArray)
			{
				this.WriteArray((Array)propertyValue, property, context);
			}
			else if (propertyType == typeof(string))
			{
				this.WriteString((string)propertyValue, property, context);
			}
			else if (propertyType.IsEnum || propertyType.IsPrimitive)
			{
				this.WriteScalarValue(propertyValue, property.PropertyType, context);
			}
			else
			{
				this.WriteScalarValue(propertyValue, property.PropertyType, context);
			}
		}

		private void WriteArray(
			Array array,
			PropertyInfo property,
			StructSerializerContext context
			)
		{
			SerializedArrayAttribute arrayAttribute = property.GetCustomAttribute<SerializedArrayAttribute>();

			Type elementType = array.GetType().GetElementType();
			bool isNullTerminated = false;
			int fixedLength = 0;
			int arrayLength = (array == null) ? 0 : array.Length;
			if (arrayAttribute != null)
			{
				switch (arrayAttribute.LengthEncoding)
				{
					case ArrayLengthEncoding.Fixed:
						fixedLength = arrayAttribute.FixedLength;
						break;
					//case ArrayLengthEncoding.ByteCountPrefix16:
					//    break;
					case ArrayLengthEncoding.ElementCountPrefix16:
						context.writer.Write((short)arrayLength);
						break;
					//case ArrayLengthEncoding.ByteCountPrefix32:
					//    break;
					case ArrayLengthEncoding.ElementCountPrefix32:
						context.writer.Write(arrayLength);
						break;
					//case ArrayLengthEncoding.BytePrefixField:
					//    break;
					//case ArrayLengthEncoding.ElementPrefixField:
					//    break;
					case ArrayLengthEncoding.ZeroTerminated:
						isNullTerminated = true;
						break;
					default:
						break;
				}
			}

			int writeElementCount =
				(fixedLength == 0)
				? arrayLength
				: Math.Min(arrayLength, fixedLength)
				;

			for (int i = 0; i < writeElementCount; i++)
			{
				object element = array.GetValue(i);
				this.WriteScalarValue(element, elementType, context);
			}
			if (writeElementCount < fixedLength || isNullTerminated)
			{
				object element = Activator.CreateInstance(elementType);

				for (int i = writeElementCount; i < fixedLength; i++)
				{
					this.WriteScalarValue(element, elementType, context);
				}
				if (isNullTerminated)
				{
					this.WriteScalarValue(element, elementType, context);
				}
			}
		}
		private void WriteString(
			string str,
			PropertyInfo property,
			StructSerializerContext context
			)
		{
			char[] chars = (str == null) ? new char[0] : str.ToCharArray();
			SerializedArrayAttribute arrayAttribute = property.GetCustomAttribute<SerializedArrayAttribute>();

			bool isNullTerminated = false;
			int fixedLength = 0;
			bool isByteCount = false;
			if (arrayAttribute != null)
			{
				switch (arrayAttribute.LengthEncoding)
				{
					case ArrayLengthEncoding.Fixed:
						fixedLength = arrayAttribute.FixedLength;
						break;
					//case ArrayLengthEncoding.ByteCountPrefix16:
					//    break;
					case ArrayLengthEncoding.ElementCountPrefix16:
						context.writer.Write((short)chars.Length);
						break;
					//case ArrayLengthEncoding.ByteCountPrefix32:
					//    break;
					case ArrayLengthEncoding.ElementCountPrefix32:
						context.writer.Write(chars.Length);
						break;
					//case ArrayLengthEncoding.BytePrefixField:
					//    break;
					//case ArrayLengthEncoding.ElementPrefixField:
					//    break;
					case ArrayLengthEncoding.ZeroTerminated:
						isNullTerminated = true;
						break;
					default:
						break;
				}
			}

			if ((fixedLength > 0) && (fixedLength != chars.Length))
			{
				char[] fixedChars = new char[fixedLength];
				Array.Copy(chars, fixedChars, Math.Min(chars.Length, fixedLength));
				chars = fixedChars;
			}
			context.writer.Write(chars);
			if (isNullTerminated)
			{
				context.writer.Write('\0');
			}
		}
		private void WriteScalarValue(
			object value,
			Type propertyType,
			StructSerializerContext context
			)
		{
			if (propertyType.IsPrimitive)
			{
				this.WritePrimitive(value, context);
			}
			else if (propertyType.IsEnum)
			{
				object numericValue = ((Enum)value).GetUnderlyingValue();
				this.WritePrimitive(numericValue, context);
			}
			else
			{
				this.WriteStruct(value, context);
			}
		}
		private void WritePrimitive(
			object value,
			StructSerializerContext context
			)
		{
			if (value == null) throw new ArgumentNullException("value");

			BinaryWriter writer = context.writer;
			if (value is int)
				writer.Write((int)value);
			else if (value is uint)
				writer.Write((uint)value);
			else if (value is short)
				writer.Write((short)value);
			else if (value is ushort)
				writer.Write((ushort)value);
			else if (value is byte)
				writer.Write((byte)value);
			else if (value is bool)
				writer.Write((bool)value);
			else if (value is long)
				writer.Write((long)value);
			else if (value is ulong)
				writer.Write((ulong)value);
			else
				throw new ArgumentException("Unknown type");
		}
		#endregion
	}
}
